//============================================================================
// Name        : Logger.cpp
// Author      : Anton Kuchin
// Version     :
// Copyright   : Your copyright notice
// Description : Logger class
//============================================================================
// TODO:
//	+ write to file
//	+ priority (err, warn, dbg)
//	+ rotate
//	- log component name
//	-/+ lockless
//	+ time
//	- stack/call trace
//
//		void log(level, msg [, ...]){}
//
//============================================================================

//#define PRINT_DEBUG_MESSAGES
#define SYNC
//#define NOFILE

#ifdef PRINT_DEBUG_MESSAGES
#define debugPrintf(...) printf(__VA_ARGS__)
#else
#define debugPrintf(...)
#endif


#include <iostream>

#include <string.h>
#include <time.h>
//#include <error.h>
#include <errno.h>
#include <stdlib.h>

//for file open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <fstream>
#include <cstdio>

#include <pthread.h>

#include <sstream>

using namespace std;

int flusherCounter = 0; // TODO: remove after debug
int atomicLogCounter = 0;
pthread_barrier_t barrier;

//simple template method that converts any printable type to string
//(is used in backup filename construction)
template<typename Type_t>
string ToStr(const Type_t& T) {
	ostringstream sout;
	sout << T;
	return (sout.str());
}

enum LOG_LEVEL {
	LOG_ERROR, //=0
	LOG_WARNING, //=1
	LOG_DEBUG, //=2
};

struct logEntry {
	time_t time; //time of event
	LOG_LEVEL level;
	//char level; //what type of event
	char msg[255]; //additional message that describes event
};

//length of string representing time of the event that is written to log file
#define TIME_STRING_LENGTH 12 //FIXME: works wrong on different architectures

//prototype of function that writes log entries from internal buffer to disk
void* initFlusher(void* arg);

//convert time_t to char[] representing time in format [HH:MM:SS]
char* timeConvert(time_t time, char* chrTime) {
	struct tm * tmTime;

	//localtime_r( &time, tmTime );
	tmTime = localtime(&time);
	strftime(chrTime, TIME_STRING_LENGTH, "[%X] ", tmTime);
	return (chrTime);
}

class Logger {
public:

//	FIXME: how to use this sh*t ???
#ifdef SYNC
	pthread_cond_t flusherCondition; // synchronization object for writer
	pthread_mutex_t flusherMutex; // synchronization object for writer
#endif

private:

	LOG_LEVEL logLevel;
	long long maxLogSize;
	FILE* pLogFile;			// pointer to file object that is being written
	logEntry* pRingBuffer;	// pointer to buffer
	int ringBufSize; 		// size of buffer in number of entries
	int iTail;				// index of next free space in buffer
	int iHead;				// index of last written entry
	pthread_t flusherThread; 		// thread that writes from buffer to disk
	string logFileName; 	// name of log file
	int filesCount; 		// number of backup files that were written
	bool timeToFinish;			// flag that indicates that main thread is ready to exit and
							// it's timw for writer thread to finish its job and exit too
	bool flusherIsAwake;		//flag that indicates that writer is awake
	pthread_mutex_t rbMutex;//mutex for ring buffer index modification

	bool writerFlag;			//helps not to deadlock when signal is missed

public:

	//default constructor for Logger object
	Logger(LOG_LEVEL logLevel = LOG_ERROR, string logFileName = "log.txt",
			int maxLogSize = 10000000, int ringBufSize = 100) {
		if (logLevel > LOG_DEBUG) {
			this->logLevel = LOG_DEBUG;
		} else {
			this->logLevel = logLevel;
		}

		this->maxLogSize = maxLogSize;

		struct stat fileStat;

		this->logFileName = logFileName;
		this->maxLogSize = maxLogSize;

		stat(logFileName.c_str(), &fileStat);

		pLogFile = fopen(logFileName.c_str(), "a+");
		if (pLogFile < 0) {
			cout << strerror(errno) << endl;
			exit(EXIT_FAILURE);
		}


		pthread_attr_t attr;
		pthread_attr_init(&attr);
		pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

		pthread_mutex_init(&rbMutex, NULL);

		timeToFinish = false;
		flusherIsAwake = false;
#ifdef SYNC
		pthread_cond_init(&flusherCondition, NULL);
		pthread_mutex_init(&flusherMutex, NULL);
#endif
		pthread_create(&flusherThread, &attr, initFlusher, this);
		//FIXME: how to wait for thread start?

		this->ringBufSize = ringBufSize;

		pRingBuffer = new logEntry[ringBufSize];

		iTail = 0;
		iHead = 0;

		filesCount = 0;
	}

	~Logger() {
		//TODO: sync buffer
		//		close file descriptors
		//		destroy sync objects
		//
		timeToFinish = true;

		debugPrintf("broadcasted signal\n");

#ifdef SYNC
		pthread_mutex_lock(&flusherMutex);
//		while(!writerIsAwake)
			pthread_cond_signal(&flusherCondition);
		pthread_mutex_unlock(&flusherMutex);
#endif
		pthread_join(flusherThread, NULL);
		debugPrintf("waited for logger exit\n");
		fclose(pLogFile);
	}

	void log(LOG_LEVEL level, char* msg) {
		if (level <= logLevel) {
			logEntry buf;

#ifdef PRINT_DEBUG_MESSAGES
			int first = 1;
#endif

			pthread_mutex_lock(&rbMutex);
			while ((iHead + ringBufSize - iTail) % ringBufSize // FIXME: FUCKIN' RACE IS HERE!!!!
					>= ringBufSize - 1) {
#ifdef PRINT_DEBUG_MESSAGES
				if (first == 1) {
					debugPrintf("log to buff sleeps because buffer is full\n");
					first = 0;
				}
#endif
			};

//			pthread_mutex_lock(&rbMutex);
			int pWrite = iHead; //XXX: atomic?
			iHead = (iHead + 1) % ringBufSize;
			atomicLogCounter++;
			pthread_mutex_unlock(&rbMutex);

			buf.time = time(NULL);
			buf.level = level;
			strcpy(buf.msg, msg);

			pRingBuffer[pWrite] = buf;

#ifdef SYNC
//			pthread_mutex_lock(&writerMutex);
//			while (!writerIsAwake){
				pthread_mutex_lock(&flusherMutex);
				debugPrintf("Flusher flag is set\n");
				writerFlag = true;
//				if (!flusherIsAwake)
					pthread_cond_signal(&flusherCondition);
//					pthread_cond_broadcast(&flusherCondition);
				debugPrintf("Condition is sent\n");
				pthread_mutex_unlock(&flusherMutex);
//			}
//			pthread_mutex_unlock(&writerMutex);
#endif
		}
	}


	//not really necessary, used for convenience of debug output
	int size() {
		return ((iHead + ringBufSize - iTail) % ringBufSize);
	}

private:
	void logFlush() {
		char chrTime[TIME_STRING_LENGTH];

		struct stat fileStat;

		do {
		stat(logFileName.c_str(), &fileStat);

		debugPrintf("File size is %d, Max filesize is %d\n", fileStat.st_size, maxLogSize);
		//check file size for log file rotation
		if (fileStat.st_size > maxLogSize) { //TODO: check status of rename
			fclose(pLogFile);
			string tempLogFileName = logFileName;

			tempLogFileName.insert(
					logFileName.find_last_of('.', logFileName.length()),
					ToStr(filesCount));

			while (access(tempLogFileName.c_str(), F_OK) == 0) {
				filesCount++;
				tempLogFileName = logFileName;
				tempLogFileName.insert(
						logFileName.find_last_of('.', logFileName.length()),
						ToStr(filesCount));
			}

			rename(logFileName.c_str(), tempLogFileName.c_str());

			filesCount++;
			pLogFile = fopen(logFileName.c_str(), "a+");
			if (pLogFile < 0) {
				cout << strerror(errno) << endl;
				exit(EXIT_FAILURE);
			}
		}



			debugPrintf("waiting for writerCondition (bs = %d)\n", size());
#ifdef SYNC
			pthread_mutex_lock(&flusherMutex);
			flusherIsAwake = false;
			if (!writerFlag ) {
				debugPrintf("Flag is not set, waiting for condition\n");
				pthread_cond_wait(&flusherCondition, &flusherMutex);
				debugPrintf("Signal recieved, writer waked up\n");
			}
			pthread_mutex_unlock(&flusherMutex);
			flusherIsAwake = true;
#endif
			while (iTail != iHead) {
				int writtenChar = 0;
				writtenChar =
				fprintf(pLogFile, "%s <%d> %s\n",
						timeConvert(pRingBuffer[iTail].time, chrTime),
						pRingBuffer[iTail].level, pRingBuffer[iTail].msg);

				if((TIME_STRING_LENGTH+strlen(pRingBuffer[iTail].msg)+5*sizeof(char)) != writtenChar){
					debugPrintf("I'VE WRITTEN _%d_ TO FILE AND I WAS TO WRITE _%d_\n", writtenChar, (TIME_STRING_LENGTH+strlen(pRingBuffer[iTail].msg)+5*sizeof(char)));
				}
				iTail = (iTail + 1) % ringBufSize; //FIXME: atomic?
				flusherCounter++;
			};
#ifdef SYNC
			debugPrintf("Flusher flag is UNset\n");
			writerFlag = false;
#endif
		} while ((!timeToFinish) || (size() != 0));
		debugPrintf("Exited: time to finish is %d, size is %d", timeToFinish, size());
#ifdef SYNC
		pthread_mutex_unlock(&flusherMutex);
#endif
	}

	friend void* initFlusher(void* arg);
};

//TODO: this way of working with threads is awful but it works, replace with waits and signals
void* initFlusher(void* arg) {
	Logger* obj = (Logger*) arg;

	debugPrintf("started writer\n");
	obj->logFlush();
	debugPrintf("finished writer\n");
	pthread_exit(NULL);
	return (NULL);
}

typedef struct argThread{
	Logger* logger;
	int threadNum;
} argThread;

#define RUNS 3
#define NUMBER_OF_TEST_THREADS 300

int testerCounter[NUMBER_OF_TEST_THREADS];

argThread argForThreads[NUMBER_OF_TEST_THREADS];

void* test(void* arg) {
	Logger* obj = ((argThread*) arg)->logger;
	int num = ((argThread*) arg)->threadNum;

	int cycles = 300;

	char msg[100] = "test log message by thread = ";

	strcat(msg, ToStr(num).c_str());

	testerCounter[num] = 0;

	pthread_barrier_wait(&barrier);

	while (cycles > 0) {
		obj->log((LOG_LEVEL) (rand() % 3), msg);
		testerCounter[num]++;
		cycles--;
		debugPrintf("TID = %d logged to buf now sleeping (bs = %d)\n", num, obj->size());
	}
	pthread_exit(NULL);
	return (NULL);
}

int main() {
	pthread_t testThreads[2001];
	pthread_attr_t attr;
	int numOfThreadsCreated = 0;

	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
	pthread_barrier_init(&barrier,NULL, NUMBER_OF_TEST_THREADS);

//	Test of multi-threading log
#ifndef NOFILE
	Logger eLogger(LOG_DEBUG,"log_rot.txt");
#else
	Logger eLogger(LOG_DEBUG,"/dev/null");
#endif

	for (int i = 0; i < NUMBER_OF_TEST_THREADS; i++) {
		argForThreads[i].logger = &eLogger;
		argForThreads[i].threadNum = i;
		numOfThreadsCreated+=pthread_create(&testThreads[i], &attr, test, &argForThreads[i]);
	}

	for (int i = 0; i < NUMBER_OF_TEST_THREADS; i++) {
		pthread_join(testThreads[i], NULL);
	}
//	End of test of multi-threading log

//// Part was added to test performance and profile
//	long * duration = new long[RUNS];
//
//	for (int c = 0; c < RUNS; c++) {
//
//		long start = time(NULL);
//
//		for (int i = 0; i < 500000; i++) {
//			eLogger.log_to_buf(DEBUG, "test_string");
//		}
//
//		long end = time(NULL);
//
//		duration[c] = end - start;
//	}
//
//	for (int c = 0; c < RUNS; c++) {
//		cout << duration[c] << endl;
//	}
//// End of performance test

	cout << "FlusherCounter" << flusherCounter << endl << "AtomicFlusherCounter" << atomicLogCounter << endl << endl;

	for (int i = 0; i < NUMBER_OF_TEST_THREADS; i++) {
		cout << i << "\t" << testerCounter[i] << endl;
	}

	debugPrintf("exited\n");

	return (0);
}

